Learn Web Performance
A comprehensive guide to web performance optimization based on web.dev's Learn Performance course
Table of Contentsβ
- Introduction
- Why Speed Matters
- General HTML Performance Considerations
- Understanding the Critical Rendering Path
- Optimize Resource Loading
- Resource Hints
- Image Performance
- Video Performance
- Web Font Optimization
- Code-Split JavaScript
- Lazy Loading
- Prefetching and Precaching
- Web Workers
- Performance Checklist
Introductionβ
Web performance is a crucial aspect of web development that focuses on two primary areas:
- Loading Speed - How quickly pages appear in the browser
- Responsiveness - How quickly pages react to user input
Optimizing for performance creates better user experiences, which directly translates to achieving your website's goalsβwhether that's increased sales, higher engagement, or improved user satisfaction.
What This Guide Coversβ
This guide focuses on web performance fundamentals that are essential for developers at all levels. Each section explores key concepts with practical, actionable techniques you can implement immediately. Topics covered include:
- Core performance concepts and metrics
- HTML, CSS, and JavaScript optimization
- Media optimization (images, videos, fonts)
- Advanced loading strategies
- Multi-threading with Web Workers
Why Speed Mattersβ
The User Experience Impactβ
Performance is not just a technical metricβit's a fundamental component of user experience. Fast websites create positive experiences that keep users engaged, satisfied, and returning.
Real-World Performance Statistics:
- Amazon found that every 100ms of latency cost them 1% in sales
- Google discovered that a 0.5-second delay in search results led to a 20% drop in traffic
- Pinterest reduced perceived wait times by 40% and saw a 15% increase in SEO traffic and sign-ups
Core Web Vitalsβ
Google's Core Web Vitals represent the most important performance metrics that affect user experience:
1. Largest Contentful Paint (LCP)β
Measures: Loading performance Good: β€ 2.5 seconds What it tracks: When the largest content element becomes visible
<!-- Optimize LCP by preloading hero images -->
<link rel="preload" as="image" href="hero-image.jpg">
2. First Input Delay (FID) / Interaction to Next Paint (INP)β
Measures: Interactivity and responsiveness Good: β€ 100ms (FID) / β€ 200ms (INP) What it tracks: Time from user interaction to browser response
3. Cumulative Layout Shift (CLS)β
Measures: Visual stability Good: β€ 0.1 What it tracks: Unexpected layout shifts during page load
<!-- Prevent CLS by specifying image dimensions -->
<img src="photo.jpg" width="800" height="600" alt="Description">
Additional Performance Metricsβ
First Contentful Paint (FCP) When the first text or image appears (Good: β€ 1.8s)
Time to Interactive (TTI) When the page becomes fully interactive (Good: β€ 3.8s)
Total Blocking Time (TBT) Total time when the main thread is blocked (Good: β€ 200ms)
Business Impactβ
Performance improvements deliver measurable business results:
- Conversion Rates: Faster sites convert better (Walmart saw 2% increase per 1s improvement)
- SEO Rankings: Page speed is a ranking factor for Google
- User Retention: 53% of mobile users abandon sites taking over 3 seconds to load
- Bounce Rates: BBC lost 10% of users for every additional second of load time
- User Satisfaction: Faster sites score higher in user satisfaction surveys
General HTML Performance Considerationsβ
Every website journey begins with an HTML request. Optimizing this foundational request is critical for overall site performance.
HTML Document Structureβ
Keep Your DOM Leanβ
A bloated DOM slows parsing, rendering, and JavaScript execution.
<!-- β Bad: Deeply nested, excessive elements -->
<div class="wrapper">
<div class="container">
<div class="inner">
<div class="content">
<p>Text</p>
</div>
</div>
</div>
</div>
<!-- β
Good: Simplified structure -->
<div class="container">
<p>Text</p>
</div>
Best Practices:
- Target < 1,500 DOM nodes
- Keep depth < 32 levels
- Avoid parent nodes with > 60 children
Semantic HTMLβ
Use semantic elements for better parsing and accessibility:
<!-- β
Semantic and performant -->
<article>
<header>
<h1>Article Title</h1>
</header>
<section>
<p>Content...</p>
</section>
</article>
HTML Caching Strategiesβ
Proper caching reduces server requests and improves repeat visit performance.
Cache-Control Headersβ
# For HTML (check for updates frequently)
Cache-Control: no-cache
# Or with revalidation
Cache-Control: max-age=3600, must-revalidate
ETags for Efficient Validationβ
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# Server returns 304 Not Modified if unchanged
Parser-Blocking Resourcesβ
Synchronous scripts halt HTML parsing, delaying page rendering.
<!-- β Blocks parsing -->
<script src="app.js"></script>
<!-- β
Defers execution until after parsing -->
<script defer src="app.js"></script>
<!-- β
Downloads in parallel, executes when ready -->
<script async src="analytics.js"></script>
When to use async vs defer:
defer: Scripts that need the full DOM (most application scripts)async: Independent scripts (analytics, ads)- Neither: Critical inline scripts that must execute immediately
Render-Blocking CSSβ
CSS blocks rendering until fully parsed and processed.
Critical CSS Inliningβ
<head>
<!-- Inline critical above-the-fold CSS -->
<style>
/* Critical styles for initial viewport */
.header { background: #333; color: white; }
.hero { min-height: 400px; }
</style>
<!-- Load remaining CSS asynchronously -->
<link rel="preload" href="styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
Media Queries for Conditional Loadingβ
<!-- Only loads for print -->
<link rel="stylesheet" href="print.css" media="print">
<!-- Only loads on large screens -->
<link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)">
Minimize and Compress HTMLβ
# Before minification (2.5 KB)
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
# After minification (1.8 KB)
<!DOCTYPE html><html><head><title>My Page</title></head><body><h1>Hello World</h1></body></html>
Tools for HTML Minification:
- HTMLMinifier
- html-minifier-terser
- Build tool plugins (Webpack, Vite, etc.)
Understanding the Critical Rendering Pathβ
The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into rendered pixels.
The Six-Step Rendering Processβ
1. Parse HTML β DOM Tree
2. Parse CSS β CSSOM Tree
3. Execute JavaScript (may modify DOM/CSSOM)
4. Combine DOM + CSSOM β Render Tree
5. Layout (calculate positions and sizes)
6. Paint (draw pixels to screen)
Step-by-Step Breakdownβ
1. DOM Constructionβ
The browser converts HTML markup into a Document Object Model:
<html>
<body>
<div>
<p>Hello World</p>
</div>
</body>
</html>
Becomes a tree structure:
html
βββ body
βββ div
βββ p
βββ "Hello World"
2. CSSOM Constructionβ
CSS is parsed into a CSS Object Model:
body { font-size: 16px; }
p { color: blue; }
Creates a CSSOM tree with computed styles.
3. JavaScript Executionβ
JavaScript can modify both DOM and CSSOM:
// Modifies DOM
document.querySelector('p').textContent = 'Updated!';
// Modifies CSSOM
document.querySelector('p').style.color = 'red';
4. Render Tree Creationβ
The browser combines DOM and CSSOM, excluding non-visible elements:
render tree = DOM tree + CSSOM tree - hidden elements
Elements like <head>, <script>, or elements with display: none are excluded.
5. Layout (Reflow)β
Calculate exact position and size of each element:
/* Browser calculates exact pixels */
.container { width: 50%; } /* If viewport is 1000px, calculates to 500px */
.box { margin: 10px; } /* Exact position considering other elements */
6. Paintβ
Draw pixels to the screen, layer by layer:
- Background colors/images
- Borders
- Text
- Shadows and effects
Optimizing the Critical Pathβ
Strategy 1: Minimize Critical Resourcesβ
<!-- β Multiple render-blocking CSS files -->
<link rel="stylesheet" href="header.css">
<link rel="stylesheet" href="navigation.css">
<link rel="stylesheet" href="content.css">
<link rel="stylesheet" href="footer.css">
<!-- β
Combined critical CSS -->
<link rel="stylesheet" href="critical.css">
<!-- Non-critical loaded async -->
<link rel="preload" href="non-critical.css" as="style"
onload="this.rel='stylesheet'">
Strategy 2: Minimize Critical Bytesβ
/* β Before minification (450 bytes) */
.header {
background-color: #333333;
padding: 20px;
margin-bottom: 10px;
}
/* β
After minification (85 bytes) */
.header{background-color:#333;padding:20px;margin-bottom:10px}
Strategy 3: Optimize Critical Path Lengthβ
<!-- β Long critical path: HTML β CSS β Font β Render -->
<link rel="stylesheet" href="styles.css">
<!-- styles.css contains: @import url('fonts.css'); -->
<!-- β
Shorter path: HTML β [CSS, Font in parallel] β Render -->
<link rel="stylesheet" href="styles.css">
<link rel="preload" href="font.woff2" as="font" crossorigin>
Measuring Critical Path Performanceβ
Use Chrome DevTools Performance panel:
- Open DevTools (F12)
- Go to Performance tab
- Record page load
- Analyze waterfall chart for blocking resources
Optimize Resource Loadingβ
Modern web pages reference numerous resourcesβCSS files, JavaScript bundles, images, fonts, and more. Strategic resource loading is essential for performance.
Understanding Resource Priorityβ
Browsers assign priority levels to resources:
| Priority | Resources |
|---|---|
| Highest | Main HTML document |
| High | CSS files, fonts, synchronous scripts |
| Medium | Async scripts, images in viewport |
| Low | Images below fold, prefetch resources |
| Lowest | Speculative prefetch |
Script Loading Strategiesβ
Default (Synchronous)β
<!-- Blocks HTML parsing, executes immediately -->
<script src="app.js"></script>
Use for: Critical scripts needed before rendering
Async Attributeβ
<!-- Downloads in parallel, executes as soon as ready -->
<script async src="analytics.js"></script>
Characteristics:
- Downloads don't block parsing
- Executes immediately when ready (may interrupt parsing)
- No guaranteed execution order
- Use for: Independent scripts (analytics, ads, trackers)
Defer Attributeβ
<!-- Downloads in parallel, executes after HTML parsing -->
<script defer src="main.js"></script>
<script defer src="utils.js"></script>
Characteristics:
- Downloads don't block parsing
- Executes after DOMContentLoaded
- Maintains script order
- Use for: Application scripts that need full DOM
Comparison Chartβ
Download Execute Blocks Parsing Order Guaranteed
--------------------------------------------------------------
none Sequential Immediate Yes Yes
async Parallel ASAP No No
defer Parallel After DOM No Yes
Module Scriptsβ
Modern JavaScript modules with built-in defer behavior:
<!-- Type="module" automatically defers -->
<script type="module" src="app.js"></script>
<!-- Module with async -->
<script type="module" async src="analytics.js"></script>
Preloading Critical Resourcesβ
Tell the browser about critical resources early:
<head>
<!-- Preload critical CSS -->
<link rel="preload" href="critical.css" as="style">
<!-- Preload critical font -->
<link rel="preload" href="font.woff2" as="font"
type="font/woff2" crossorigin>
<!-- Preload critical script -->
<link rel="preload" href="app.js" as="script">
<!-- Preload hero image -->
<link rel="preload" href="hero.jpg" as="image">
</head>
Important: Only preload truly critical resources (typically 2-3 items).
Resource Compressionβ
Enable Compressionβ
Server configuration for compression:
# Nginx configuration
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1000;
# Or use Brotli (better compression)
brotli on;
brotli_types text/css application/javascript application/json;
Compression Comparisonβ
Original JavaScript file: 150 KB
Gzip compressed: 45 KB (70% reduction)
Brotli compressed: 38 KB (75% reduction)
Minificationβ
Remove unnecessary characters without changing functionality:
// Before minification (250 bytes)
function calculateTotal(items) {
let total = 0;
for (let item of items) {
total += item.price;
}
return total;
}
// After minification (98 bytes)
function calculateTotal(t){let e=0;for(let l of t)e+=l.price;return e}
Tools:
- Terser (JavaScript)
- cssnano (CSS)
- HTMLMinifier (HTML)
Bundle Optimizationβ
Code Splittingβ
Break large bundles into smaller chunks:
// Instead of one large bundle
import everything from './everything.js'; // 500 KB
// Split into smaller chunks
import('./feature-a.js'); // 50 KB - loads when needed
import('./feature-b.js'); // 75 KB - loads when needed
import('./feature-c.js'); // 100 KB - loads when needed
Tree Shakingβ
Remove unused code during build:
// utils.js exports 10 functions
export { func1, func2, func3, /* ... */ func10 };
// app.js only imports one
import { func1 } from './utils.js';
// Build process removes func2-func10 from final bundle
Requirements for tree shaking:
- Use ES6 module syntax (
import/export) - Avoid side effects in modules
- Configure bundler properly
Resource Hintsβ
Resource hints are HTML directives that help browsers make intelligent decisions about resource loading priorities.
DNS Prefetchβ
Resolve domain names before they're needed:
<!-- Resolve DNS for third-party domains -->
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.example.com">
When to use:
- Third-party API domains
- CDN domains
- Analytics domains
- Any external domain used later in page
Benefits:
- Saves 20-120ms per domain
- Works on all browsers
- Minimal overhead
Preconnectβ
Establish full connection (DNS + TCP + TLS) to critical origins:
<!-- Full connection setup for critical resources -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
When to use:
- Critical third-party resources
- Resources needed within 1-2 seconds
- Limit to 2-3 most important origins
Benefits:
- Saves 100-500ms per connection
- Includes TLS handshake
- More thorough than dns-prefetch
Trade-off: Higher resource cost than dns-prefetch
Prefetchβ
Download resources for next navigation:
<!-- Load resources for likely next page -->
<link rel="prefetch" href="next-page.html">
<link rel="prefetch" href="next-page.js">
<link rel="prefetch" href="next-page.css">
When to use:
- Next page in a sequence (paginated content)
- Common user flows (product β checkout)
- Dashboard resources after login
Characteristics:
- Lowest priority fetch
- Uses idle network time
- Cached for future use
- Doesn't execute/parse
Preloadβ
High-priority fetch for current page resources:
<!-- Critical resources for THIS page -->
<link rel="preload" href="hero-font.woff2" as="font"
type="font/woff2" crossorigin>
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.jpg" as="image">
When to use:
- Critical fonts
- Above-the-fold images
- Critical CSS
- Important scripts discovered late
Important: Don't overuse! Only 2-3 truly critical resources.
Prerender (Deprecated) β Speculation Rules APIβ
The modern replacement for prerender:
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["/next-page"]
}
],
"prefetch": [
{
"source": "document",
"where": {
"href_matches": "/articles/*"
}
}
]
}
</script>
Resource Hints Decision Treeβ
Need DNS resolution only?
ββ Yes β dns-prefetch
ββ No β Need full connection?
ββ Yes β preconnect
ββ No β For current page?
ββ Yes β preload
ββ No β prefetch
Practical Exampleβ
<!DOCTYPE html>
<html>
<head>
<!-- DNS resolution for analytics (not critical) -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<!-- Full connection for critical fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload critical assets for THIS page -->
<link rel="preload" href="hero-font.woff2" as="font"
type="font/woff2" crossorigin>
<link rel="preload" href="hero-image.jpg" as="image">
<!-- Prefetch likely next navigation -->
<link rel="prefetch" href="about.html">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Content -->
</body>
</html>
Image Performanceβ
Images typically account for 50-70% of a webpage's total weight. Optimizing them is crucial for performance.
Choosing the Right Formatβ
| Format | Best For | Transparency | Animation | Compression |
|---|---|---|---|---|
| JPEG | Photographs | No | No | Lossy |
| PNG | Graphics, logos | Yes | No | Lossless |
| WebP | General purpose | Yes | Yes | Both |
| AVIF | Modern browsers | Yes | Yes | Superior |
| SVG | Icons, logos | Yes | Yes | Vector |
| GIF | Simple animations | Yes | Yes | Poor |
Modern Image Formatsβ
WebPβ
Google's format with 25-35% better compression than JPEG:
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
Support: 95%+ of browsers
AVIFβ
Next-generation format with 50% better compression than JPEG:
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
Support: 80%+ of browsers (growing)
Responsive Imagesβ
Serve appropriately sized images for different devices:
Using srcsetβ
<img
src="image-800.jpg"
srcset="image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
alt="Responsive image example">
How it works:
- Browser knows viewport width
- Looks at
sizesattribute to determine image display size - Picks most appropriate image from
srcset
Using picture Elementβ
More control with art direction:
<picture>
<!-- Mobile: cropped/portrait -->
<source media="(max-width: 799px)"
srcset="mobile-image.jpg">
<!-- Tablet: medium crop -->
<source media="(max-width: 1199px)"
srcset="tablet-image.jpg">
<!-- Desktop: full landscape -->
<img src="desktop-image.jpg" alt="Art direction example">
</picture>
Image Optimization Techniquesβ
1. Compressionβ
# JPEG optimization (quality 80-85)
Original: 2.5 MB
Optimized: 250 KB (90% reduction)
# PNG optimization
Original: 500 KB
Optimized: 150 KB (70% reduction)
Tools:
- ImageOptim (GUI, macOS)
- Squoosh (Web-based, Google)
- Sharp (Node.js library)
- imagemin (CLI)
2. Proper Dimensionsβ
<!-- β Bad: Loading 3000Γ2000 image for 300Γ200 display -->
<img src="huge-image.jpg" style="width: 300px; height: 200px">
<!-- β
Good: Image sized appropriately -->
<img src="small-image.jpg" width="300" height="200">
3. Set Explicit Dimensionsβ
Prevent layout shift:
<!-- β
Prevents CLS -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- Or with CSS aspect ratio -->
<img src="photo.jpg" style="aspect-ratio: 4/3; width: 100%;" alt="Photo">
4. Lazy Loadingβ
Defer offscreen images:
<img src="image.jpg" loading="lazy" alt="Lazy loaded image">
(More details in Lazy Loading section)
Image CDNsβ
Modern CDNs provide automatic optimization:
<!-- Cloudinary example -->
<img src="https://res.cloudinary.com/demo/image/upload/
w_400,f_auto,q_auto/sample.jpg">
<!-- Parameters: -->
<!-- w_400: Width 400px -->
<!-- f_auto: Automatic format (WebP/AVIF) -->
<!-- q_auto: Automatic quality -->
Popular Image CDNs:
- Cloudinary
- Imgix
- ImageKit
- Cloudflare Images
SVG Optimizationβ
Optimize vector graphics:
<!-- Before optimization (500 bytes) -->
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<!-- Unnecessary metadata -->
<title>My Icon</title>
<desc>An icon description</desc>
<circle cx="50" cy="50" r="40" fill="#333" />
</svg>
<!-- After optimization (180 bytes) -->
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="#333"/>
</svg>
SVG Optimization Tools:
- SVGO
- SVGOMG (web interface)
Image Performance Checklistβ
- Use modern formats (WebP, AVIF)
- Implement responsive images
- Compress images (80-85% quality for JPEG)
- Set explicit width and height
- Use lazy loading for below-fold images
- Consider image CDN
- Optimize SVGs
- Remove image metadata (EXIF)
Video Performanceβ
Video content is increasingly common on web pages but can significantly impact performance if not optimized properly.
Video Format Selectionβ
| Format | Codec | Browser Support | Quality |
|---|---|---|---|
| MP4 | H.264 | Universal | Good |
| WebM | VP9 | 95% | Better |
| WebM | AV1 | 70%+ | Best |
Multi-Format Strategyβ
<video controls>
<!-- Modern browsers: Best compression -->
<source src="video.webm" type="video/webm; codecs=av01.0.05M.08">
<!-- Fallback: Wide support -->
<source src="video.webm" type="video/webm; codecs=vp9">
<!-- Fallback: Universal -->
<source src="video.mp4" type="video/mp4">
Your browser doesn't support video.
</video>
Video Attributes for Performanceβ
Preload Attributeβ
<!-- Don't load anything until user plays -->
<video preload="none" poster="thumbnail.jpg">
<source src="video.mp4" type="video/mp4">
</video>
<!-- Load only metadata (duration, dimensions) -->
<video preload="metadata">
<source src="video.mp4" type="video/mp4">
</video>
<!-- Load entire video (rarely recommended) -->
<video preload="auto">
<source src="video.mp4" type="video/mp4">
</video>
Recommendation: Use preload="none" for most cases
Poster Imageβ
Provide a thumbnail while video loads:
<video preload="none" poster="thumbnail.jpg" controls>
<source src="video.mp4" type="video/mp4">
</video>
Best practices:
- Optimize poster image (WebP format)
- Match video aspect ratio
- Show representative frame
Lazy Loading Videosβ
<!-- Native lazy loading for video posters -->
<video preload="none" poster="thumbnail.jpg" loading="lazy">
<source src="video.mp4" type="video/mp4">
</video>
For more control, use Intersection Observer:
const videoObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const video = entry.target;
video.src = video.dataset.src;
video.load();
videoObserver.unobserve(video);
}
});
});
document.querySelectorAll('video[data-src]').forEach(video => {
videoObserver.observe(video);
});
Embedded Videos (YouTube, Vimeo)β
Embedded videos can be heavyβuse facade pattern:
<!-- Lightweight thumbnail with play button -->
<div class="video-facade" data-video-id="dQw4w9WgXcQ">
<img src="thumbnail.jpg" alt="Video thumbnail">
<button class="play-button">Play</button>
</div>
<script>
document.querySelectorAll('.video-facade').forEach(facade => {
facade.addEventListener('click', function() {
const videoId = this.dataset.videoId;
const iframe = document.createElement('iframe');
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
iframe.allow = 'autoplay; fullscreen';
this.replaceWith(iframe);
});
});
</script>
Benefits:
- Saves ~500KB per video
- Faster initial page load
- Loads iframe only when user engages
Video Compressionβ
Optimize video file size:
# Using FFmpeg
ffmpeg -i input.mp4 \
-c:v libx264 \ # H.264 codec
-crf 23 \ # Quality (lower = better, 18-28 range)
-preset slow \ # Encoding speed vs compression
-c:a aac \ # Audio codec
-b:a 128k \ # Audio bitrate
output.mp4
# For WebM/VP9
ffmpeg -i input.mp4 \
-c:v libvpx-vp9 \
-crf 30 \
-b:v 0 \
output.webm
Adaptive Streamingβ
For longer videos, use adaptive bitrate streaming:
HLS (HTTP Live Streaming)
<video controls>
<source src="video.m3u8" type="application/x-mpegURL">
</video>
DASH (Dynamic Adaptive Streaming)
<video controls>
<source src="video.mpd" type="application/dash+xml">
</video>
Benefits:
- Adjusts quality based on bandwidth
- Reduces buffering
- Better user experience
Libraries:
- Video.js
- Plyr
- hls.js (HLS playback)
- dash.js (DASH playback)
Replace Videos with Animated Imagesβ
For short, simple animations:
<!-- Instead of video (500 KB) -->
<video autoplay loop muted playsinline>
<source src="animation.mp4">
</video>
<!-- Use GIF alternative (100 KB) -->
<img src="animation.gif" alt="Animation">
<!-- Or better: Animated WebP (50 KB) -->
<img src="animation.webp" alt="Animation">
Video Performance Checklistβ
- Use
preload="none"by default - Provide optimized poster images
- Implement lazy loading for videos
- Use facade pattern for embedded videos
- Compress videos appropriately
- Provide multiple formats (MP4, WebM)
- Consider adaptive streaming for long videos
- Replace short videos with animated images when possible
Web Font Optimizationβ
Web fonts enhance design but can significantly impact performance if not optimized properly.
The Font Loading Problemβ
When fonts load, browsers handle the rendering in different ways:
FOIT (Flash of Invisible Text)
- Text is invisible while font loads
- Default in some browsers
- Poor user experience
FOUT (Flash of Unstyled Text)
- System font shown first, then swaps to web font
- Can cause layout shift
- Better than FOIT
Font-Display Strategyβ
Control font rendering behavior with font-display:
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* or optional, fallback, block */
}
Font-Display Valuesβ
swap (Recommended for most cases)
font-display: swap;
- Shows fallback immediately
- Swaps when custom font loads
- No invisible text
- May cause layout shift
optional (Best for performance)
font-display: optional;
- Extremely short block period (~100ms)
- If font doesn't load quickly, uses fallback
- No font swap on slow connections
- Best for body text
fallback
font-display: fallback;
- Short block period (~100ms)
- Short swap period (~3s)
- After 3s, uses fallback permanently
- Balanced approach
block
font-display: block;
- Text invisible up to 3 seconds
- Not recommended
- Only for critical brand fonts
When to Use Eachβ
/* Body text: Use optional */
@font-face {
font-family: 'BodyFont';
src: url('body.woff2') format('woff2');
font-display: optional;
}
/* Headings: Use swap */
@font-face {
font-family: 'HeadingFont';
src: url('heading.woff2') format('woff2');
font-display: swap;
}
/* Icons: Use block (must be visible) */
@font-face {
font-family: 'Icons';
src: url('icons.woff2') format('woff2');
font-display: block;
}
Preload Critical Fontsβ
Load fonts before CSS is parsed:
<head>
<link rel="preload"
href="critical-font.woff2"
as="font"
type="font/woff2"
crossorigin>
<link rel="stylesheet" href="styles.css">
</head>
Important:
- Only preload 1-2 truly critical fonts
- Must include
crossoriginattribute - Specify correct
type
Font Subsettingβ
Include only characters you need:
# Full font: 150 KB
# Subset (Latin only): 30 KB (80% reduction)
# Using glyphhanger
glyphhanger --subset=font.ttf --formats=woff2
What to subset:
- Language characters (Latin, Cyrillic, etc.)
- Specific glyphs needed
- Remove unused ligatures and features
Tools:
- glyphhanger
- fonttools (pyftsubset)
- Online subsetting tools
Variable Fontsβ
One file with multiple weights/styles:
/* Traditional: Multiple files */
@font-face {
font-family: 'Regular';
src: url('regular.woff2');
font-weight: 400;
}
@font-face {
font-family: 'Bold';
src: url('bold.woff2');
font-weight: 700;
}
/* Total: 100 KB */
/* Variable font: One file */
@font-face {
font-family: 'Variable';
src: url('variable.woff2');
font-weight: 100 900; /* Full range */
}
/* Total: 60 KB (40% reduction) */
Benefits:
- Fewer HTTP requests
- Smaller total file size
- Infinite weight variations
- Smooth animations
Usage:
h1 { font-weight: 750; } /* Any value between 100-900 */
h2 { font-weight: 625; }
Font Format Selectionβ
Always provide WOFF2:
@font-face {
font-family: 'MyFont';
src: url('font.woff2') format('woff2'); /* 95%+ browser support */
}
Format comparison:
TTF: 100 KB (uncompressed)
WOFF: 60 KB (40% compression)
WOFF2: 45 KB (55% compression) β
Use this
Self-Hosting vs. Google Fontsβ
Self-Hosting Advantagesβ
<link rel="preload" href="/fonts/font.woff2" as="font" crossorigin>
<style>
@font-face {
font-family: 'MyFont';
src: url('/fonts/font.woff2') format('woff2');
font-display: swap;
}
</style>
Pros:
- Full control over caching
- No external requests
- Privacy-friendly
- Can preload fonts
Google Fontsβ
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap"
rel="stylesheet">
Pros:
- Easy implementation
- May be cached from other sites
- Automatic optimization
Cons:
- External request
- Less control
- Privacy concerns in some regions
Reduce Layout Shift from Font Loadingβ
Match fallback font metrics:
@font-face {
font-family: 'CustomFont';
src: url('custom.woff2') format('woff2');
font-display: swap;
/* Modern: use size-adjust */
size-adjust: 95%;
ascent-override: 90%;
descent-override: 20%;
}
body {
font-family: 'CustomFont', Arial, sans-serif;
}
Or use Font Face Observer:
const font = new FontFaceObserver('CustomFont');
font.load().then(() => {
document.documentElement.classList.add('fonts-loaded');
});
/* Before fonts load */
body {
font-family: Arial, sans-serif;
}
/* After fonts load */
.fonts-loaded body {
font-family: 'CustomFont', Arial, sans-serif;
}
Font Loading Best Practicesβ
/* β
Complete optimized setup */
@font-face {
font-family: 'OptimizedFont';
src: url('font.woff2') format('woff2');
font-display: swap;
font-weight: 400 700; /* Variable font range */
unicode-range: U+0000-00FF; /* Latin subset */
}
Web Font Checklistβ
- Use
font-display: swaporoptional - Preload critical fonts only (1-2 max)
- Subset fonts to needed characters
- Use WOFF2 format
- Consider variable fonts
- Limit number of font weights/styles
- Match fallback font metrics
- Self-host when possible
Code-Split JavaScriptβ
JavaScript often represents the largest contributor to page bloat. Code splitting helps load only what's needed.
Why Code Splitting Mattersβ
// β Problem: One massive bundle
import everything from './everything.js'; // 800 KB
// User downloads 800 KB even if they only use 10%
Issues:
- Longer download time
- More parsing/compilation
- Delayed interactivity
- Poor cache utilization
Dynamic Importsβ
Load code on demand:
// β
Load feature only when needed
button.addEventListener('click', async () => {
const module = await import('./heavy-feature.js');
module.initFeature();
});
Benefits:
- Smaller initial bundle
- Faster Time to Interactive
- Better performance
Route-Based Splittingβ
Split by application routes:
// React Router example
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Products = lazy(() => import('./routes/Products'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Component-Based Splittingβ
Split large UI components:
// Heavy chart component loaded only when tab opens
const Chart = lazy(() => import('./Chart'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('summary');
return (
<div>
<Tabs onChange={setActiveTab} />
{activeTab === 'chart' && (
<Suspense fallback={<Spinner />}>
<Chart />
</Suspense>
)}
</div>
);
}
Vendor Splittingβ
Separate third-party code:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// Vendor code
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
// Common code used across pages
common: {
minChunks: 2,
chunks: 'async',
name: 'common',
},
},
},
},
};
Result:
app.js (50 KB) - Your code
vendors.js (200 KB) - Third-party libraries (cached longer)
common.js (30 KB) - Shared code
Prefetch Next Routesβ
Load likely next pages during idle time:
// Prefetch product page when hovering on product card
productCard.addEventListener('mouseenter', () => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/product-page-bundle.js';
document.head.appendChild(link);
});
Or with webpack magic comments:
// Prefetch this chunk
button.addEventListener('click', async () => {
const module = await import(
/* webpackPrefetch: true */
'./next-feature.js'
);
module.init();
});
Bundle Analysisβ
Visualize your bundle composition:
# webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# In webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
Generates interactive visualization showing:
- Size of each module
- Which chunks contain what code
- Opportunities for optimization
Code Splitting Strategiesβ
1. Split by Critical Path
// Critical: Load immediately
import './critical-analytics.js';
import './essential-ui.js';
// Non-critical: Load later
setTimeout(() => {
import('./non-critical-features.js');
}, 3000);
2. Split by User Interaction
// Load only when user interacts
showModal.addEventListener('click', async () => {
const { Modal } = await import('./modal.js');
new Modal().open();
});
3. Split by Viewport
// Load when component enters viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
const { initFooter } = await import('./footer.js');
initFooter();
observer.disconnect();
}
});
});
observer.observe(document.querySelector('.footer'));
Modern vs. Legacy Bundlesβ
Serve different bundles to modern vs. old browsers:
<!-- Modern browsers -->
<script type="module" src="modern.js"></script>
<!-- Legacy browsers -->
<script nomodule src="legacy.js"></script>
Benefits:
- Modern browsers get smaller bundles
- No transpilation overhead for ES6+
- ~20-30% size reduction for modern browsers
Code Splitting Checklistβ
- Implement route-based splitting
- Split large components/features
- Separate vendor code
- Use dynamic imports for heavy features
- Prefetch likely next routes
- Analyze bundle composition
- Consider modern/legacy split
- Monitor bundle sizes in CI/CD
Lazy Loadingβ
Defer loading of non-critical resources until they're needed, typically when they enter the viewport.
Native Image Lazy Loadingβ
Modern browsers support native lazy loading:
<!-- Lazy load images below the fold -->
<img src="image.jpg" loading="lazy" alt="Description">
<!-- Eager load above the fold (default) -->
<img src="hero.jpg" loading="eager" alt="Hero image">
Browser support: 95%+ (all modern browsers)
Native Iframe Lazy Loadingβ
<!-- Lazy load embedded content -->
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
loading="lazy"
title="Video title"></iframe>
When to Lazy Loadβ
β Do lazy load:
- Images below the fold
- Images in carousels (non-active slides)
- Background images via CSS
- Video content
- Iframes (maps, social embeds)
- Comments sections
- Ads
β Don't lazy load:
- Above-the-fold images
- Critical hero images
- Logo images
- Small UI icons
- CSS files
- Critical JavaScript
Intersection Observer APIβ
For more control and older browser support:
// Create observer
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// Replace data-src with src
img.src = img.dataset.src;
// Optional: Add srcset
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
// Remove data attributes
delete img.dataset.src;
delete img.dataset.srcset;
// Stop observing this image
observer.unobserve(img);
}
});
}, {
// Load images 50px before they enter viewport
rootMargin: '50px 0px',
threshold: 0.01
});
// Observe all images with data-src
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
HTML:
<img data-src="image.jpg"
data-srcset="image-400.jpg 400w, image-800.jpg 800w"
alt="Lazy loaded image">
Lazy Loading with Placeholdersβ
Improve perceived performance with placeholders:
Low-Quality Image Placeholder (LQIP)β
<img src="tiny-placeholder.jpg"
data-src="full-image.jpg"
alt="Image with LQIP"
class="lazy-image">
<style>
.lazy-image {
filter: blur(20px);
transition: filter 0.3s;
}
.lazy-image.loaded {
filter: blur(0);
}
</style>
<script>
img.addEventListener('load', () => {
img.classList.add('loaded');
});
</script>
CSS Gradient Placeholderβ
.image-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
}
.image-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
BlurHash or ThumbHashβ
// Generate tiny placeholder hash server-side
<img src="data:image/blurred-hash-here"
data-src="full-image.jpg"
alt="Image">
Lazy Loading Background Imagesβ
<div class="hero" data-bg="hero-image.jpg"></div>
<script>
const bgObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
element.style.backgroundImage = `url(${element.dataset.bg})`;
bgObserver.unobserve(element);
}
});
});
document.querySelectorAll('[data-bg]').forEach(el => {
bgObserver.observe(el);
});
</script>
Lazy Loading with Loading Libraryβ
For production use, consider libraries with fallbacks:
// Using lazysizes library
<script src="lazysizes.min.js" async></script>
<img data-src="image.jpg"
data-srcset="image-400.jpg 400w, image-800.jpg 800w"
class="lazyload"
alt="Description">
Popular libraries:
- lazysizes (most popular)
- vanilla-lazyload
- lozad.js (lightweight)
Lazy Loading Content Sectionsβ
Defer entire sections of content:
const sectionObserver = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
const section = entry.target;
// Fetch content
const response = await fetch(section.dataset.contentUrl);
const html = await response.text();
// Insert content
section.innerHTML = html;
sectionObserver.unobserve(section);
}
});
});
document.querySelectorAll('.lazy-section').forEach(section => {
sectionObserver.observe(section);
});
<div class="lazy-section"
data-content-url="/api/comments">
<p>Loading comments...</p>
</div>
Progressive Image Loadingβ
Load images in stages for perceived performance:
class ProgressiveImage {
constructor(img) {
this.img = img;
this.loadLowQuality();
}
loadLowQuality() {
const lowQualitySrc = this.img.dataset.lowsrc;
const img = new Image();
img.onload = () => {
this.img.src = lowQualitySrc;
this.img.classList.add('low-quality-loaded');
this.loadHighQuality();
};
img.src = lowQualitySrc;
}
loadHighQuality() {
const highQualitySrc = this.img.dataset.highsrc;
const img = new Image();
img.onload = () => {
this.img.src = highQualitySrc;
this.img.classList.add('high-quality-loaded');
};
img.src = highQualitySrc;
}
}
document.querySelectorAll('.progressive-image').forEach(img => {
new ProgressiveImage(img);
});
Lazy Loading Best Practicesβ
1. Set dimensions to prevent layout shift:
<img src="placeholder.jpg"
data-src="real-image.jpg"
width="800"
height="600"
loading="lazy"
alt="Description">
2. Load slightly before entering viewport:
{
rootMargin: '50px 0px' // Load 50px before visible
}
3. Provide meaningful loading state:
<div class="image-container">
<img data-src="image.jpg" alt="Description">
<div class="loading-spinner"></div>
</div>
4. Handle errors gracefully:
img.addEventListener('error', () => {
img.src = 'fallback-image.jpg';
});
Lazy Loading Checklistβ
- Use native
loading="lazy"when possible - Implement Intersection Observer for advanced cases
- Don't lazy load above-the-fold images
- Set explicit dimensions (prevent CLS)
- Provide loading placeholders
- Load images slightly before viewport entry
- Test on slow connections
- Handle error states
Prefetching and Precachingβ
Load resources before they're needed to provide instant experiences.
Prefetchingβ
Download resources for future navigation during idle time:
<!-- Prefetch next page -->
<link rel="prefetch" href="next-page.html">
<!-- Prefetch JavaScript for next page -->
<link rel="prefetch" href="next-page-bundle.js">
<!-- Prefetch API data -->
<link rel="prefetch" href="/api/products" as="fetch">
Characteristics:
- Lowest priority
- Uses idle network time
- Cached for future use
- Doesn't execute/parse
When to Prefetchβ
β Good candidates:
- Next page in pagination
- Common user flows (login β dashboard)
- Search results β detail pages
- Product listing β product detail
Implementation example:
// Prefetch on hover
document.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('mouseenter', () => {
const productUrl = card.dataset.url;
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = productUrl;
document.head.appendChild(link);
}, { once: true });
});
Speculation Rules APIβ
Modern replacement for prerender with granular control:
<script type="speculationrules">
{
"prefetch": [
{
"source": "list",
"urls": ["/page1", "/page2"]
}
],
"prerender": [
{
"source": "list",
"urls": ["/most-likely-next-page"]
}
]
}
</script>
Advanced rules:
<script type="speculationrules">
{
"prefetch": [
{
"source": "document",
"where": {
"and": [
{ "href_matches": "/articles/*" },
{ "not": { "href_matches": "/articles/old/*" }}
]
},
"eagerness": "moderate"
}
]
}
</script>
Eagerness levels:
immediate: Prefetch right awayeager: Prefetch when link is added to pagemoderate: Prefetch on hover (200ms)conservative: Prefetch on click/tap down
Service Worker Precachingβ
Cache assets during service worker installation:
// service-worker.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.svg',
'/fonts/main.woff2'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
Caching Strategiesβ
1. Cache First (Static Assets)β
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
});
Best for: Images, CSS, JS, fonts
2. Network First (Dynamic Content)β
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
);
});
Best for: API requests, user-generated content
3. Stale-While-Revalidateβ
self.addEventListener('fetch', event => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
});
})
);
});
Best for: Balance between freshness and speed
Workbox for Service Workersβ
Simplify service worker implementation:
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
// Precache assets
precacheAndRoute(self.__WB_MANIFEST);
// Cache images
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
}),
],
})
);
// Cache API requests
registerRoute(
({url}) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api',
})
);
Predictive Prefetchingβ
Use analytics to predict user navigation:
// Track user behavior
const navigationPatterns = {
'/': ['/products', '/about'],
'/products': ['/products/item-1', '/products/item-2'],
};
function predictivelyPrefetch(currentPage) {
const likelyNextPages = navigationPatterns[currentPage] || [];
likelyNextPages.forEach(url => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
});
}
// Call on page load
predictivelyPrefetch(window.location.pathname);
Prefetching Best Practicesβ
1. Prioritize correctly:
<!-- High priority: Preload for current page -->
<link rel="preload" href="critical.js" as="script">
<!-- Low priority: Prefetch for next page -->
<link rel="prefetch" href="next-page.js">
2. Limit prefetch resources:
// Only prefetch 2-3 most likely next pages
const topPredictions = predictions.slice(0, 3);
3. Respect user preferences:
if ('connection' in navigator) {
const connection = navigator.connection;
// Don't prefetch on slow/expensive connections
if (connection.effectiveType === '4g' &&
!connection.saveData) {
prefetchResources();
}
}
4. Monitor cache size:
// Clear old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
Prefetching and Precaching Checklistβ
- Implement prefetch for likely next pages
- Use Speculation Rules API when supported
- Set up service worker precaching
- Choose appropriate caching strategies
- Respect user data preferences
- Monitor and limit cache size
- Test offline functionality
- Clear old caches on updates
Web Workersβ
Offload computationally expensive tasks from the main thread to prevent UI freezing.
Understanding the Main Thread Problemβ
Everything in the browser typically runs on one thread:
Main Thread:
βββ Parse HTML
βββ Parse CSS
βββ Execute JavaScript
βββ Layout calculations
βββ Paint operations
βββ Handle user interactions
Problem: Heavy JavaScript blocks everything else.
// β Blocks main thread for 3 seconds
function heavyCalculation() {
let result = 0;
for (let i = 0; i < 10000000000; i++) {
result += Math.sqrt(i);
}
return result;
}
button.addEventListener('click', () => {
const result = heavyCalculation(); // UI freezes!
displayResult(result);
});
What Are Web Workers?β
Separate threads for JavaScript execution:
Main Thread Worker Thread
βββ UI interactions ββ βββ Heavy calculations
βββ DOM manipulation βββ Data processing
βββ User events βββ Complex algorithms
Creating a Web Workerβ
main.js:
// Create worker
const worker = new Worker('worker.js');
// Send data to worker
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });
// Receive data from worker
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
displayResult(event.data);
};
// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Terminate worker when done
// worker.terminate();
worker.js:
// Listen for messages from main thread
self.onmessage = (event) => {
const { numbers } = event.data;
// Perform heavy calculation
const result = complexCalculation(numbers);
// Send result back to main thread
self.postMessage(result);
};
function complexCalculation(numbers) {
// Heavy processing here
return numbers.reduce((sum, num) => sum + Math.sqrt(num), 0);
}
Real-World Use Casesβ
1. Image Processingβ
// main.js
const imageWorker = new Worker('image-worker.js');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
imageWorker.postMessage({
imageData: event.target.result,
filter: 'grayscale'
});
};
reader.readAsDataURL(file);
});
imageWorker.onmessage = (event) => {
displayProcessedImage(event.data);
};
// image-worker.js
self.onmessage = (event) => {
const { imageData, filter } = event.data;
// Create image from data
const img = new Image();
img.onload = () => {
const canvas = new OffscreenCanvas(img.width, img.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// Apply filter
applyFilter(imageData, filter);
ctx.putImageData(imageData, 0, 0);
canvas.convertToBlob().then(blob => {
self.postMessage(blob);
});
};
img.src = imageData;
};
function applyFilter(imageData, filter) {
const data = imageData.data;
if (filter === 'grayscale') {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
}
}
2. Data Processingβ
// main.js
const dataWorker = new Worker('data-worker.js');
// Process large CSV dataset
fetch('large-dataset.csv')
.then(response => response.text())
.then(csvData => {
dataWorker.postMessage({
data: csvData,
operation: 'analyze'
});
});
dataWorker.onmessage = (event) => {
const { stats, sortedData } = event.data;
displayStatistics(stats);
renderChart(sortedData);
};
// data-worker.js
self.onmessage = (event) => {
const { data, operation } = event.data;
if (operation === 'analyze') {
const rows = parseCSV(data);
const stats = calculateStatistics(rows);
const sortedData = sortData(rows);
self.postMessage({ stats, sortedData });
}
};
function parseCSV(csv) {
// Parse CSV data
return csv.split('\n').map(row => row.split(','));
}
function calculateStatistics(rows) {
// Complex calculations
return {
total: rows.length,
average: rows.reduce((sum, row) => sum + parseFloat(row[1]), 0) / rows.length,
// More stats...
};
}
3. Real-Time Calculationsβ
// main.js - Game physics calculations
const physicsWorker = new Worker('physics-worker.js');
// Send game state to worker
function gameLoop() {
physicsWorker.postMessage({
entities: gameEntities,
deltaTime: dt
});
}
physicsWorker.onmessage = (event) => {
// Update entities with calculated positions
gameEntities = event.data.entities;
render();
};
// physics-worker.js
self.onmessage = (event) => {
const { entities, deltaTime } = event.data;
// Calculate physics for all entities
entities.forEach(entity => {
entity.velocity.y += gravity * deltaTime;
entity.position.x += entity.velocity.x * deltaTime;
entity.position.y += entity.velocity.y * deltaTime;
// Collision detection
checkCollisions(entity, entities);
});
self.postMessage({ entities });
};
Shared Workersβ
Share a single worker instance across multiple browser tabs:
// main.js (in multiple tabs)
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.onmessage = (event) => {
console.log('Message from shared worker:', event.data);
};
sharedWorker.port.postMessage('Hello from tab');
// shared-worker.js
const connections = [];
self.onconnect = (event) => {
const port = event.ports[0];
connections.push(port);
port.onmessage = (event) => {
// Broadcast to all connected tabs
connections.forEach(connection => {
connection.postMessage(`Broadcast: ${event.data}`);
});
};
};
Use cases:
- Shared state across tabs
- Single WebSocket connection
- Coordinated background tasks
Service Workersβ
Special workers for network requests and caching (covered in Prefetching section):
// Register service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered');
});
}
Worker Limitationsβ
Cannot access:
- DOM
windowobjectdocumentobjectparentobject
Can access:
navigatorobjectfetch()API- IndexedDB
- WebSockets
- Timers (
setTimeout,setInterval) - Import scripts via
importScripts()
Transferable Objectsβ
Efficiently transfer data without copying:
// β Slow: Copies 100MB array
const largeArray = new Uint8Array(100 * 1024 * 1024);
worker.postMessage(largeArray);
// β
Fast: Transfers ownership (zero-copy)
const largeArray = new Uint8Array(100 * 1024 * 1024);
worker.postMessage(largeArray, [largeArray.buffer]);
// Note: largeArray is now empty in main thread
Transferable types:
- ArrayBuffer
- MessagePort
- ImageBitmap
- OffscreenCanvas
Worker Poolsβ
Manage multiple workers for parallel processing:
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workers = [];
this.taskQueue = [];
this.availableWorkers = [];
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
this.workers.push(worker);
this.availableWorkers.push(worker);
worker.onmessage = (event) => {
this.handleWorkerComplete(worker, event.data);
};
}
}
runTask(data) {
return new Promise((resolve) => {
const task = { data, resolve };
if (this.availableWorkers.length > 0) {
this.executeTask(task);
} else {
this.taskQueue.push(task);
}
});
}
executeTask(task) {
const worker = this.availableWorkers.pop();
worker.currentTask = task;
worker.postMessage(task.data);
}
handleWorkerComplete(worker, result) {
worker.currentTask.resolve(result);
if (this.taskQueue.length > 0) {
const nextTask = this.taskQueue.shift();
this.executeTask(nextTask);
} else {
this.availableWorkers.push(worker);
}
}
terminate() {
this.workers.forEach(worker => worker.terminate());
}
}
// Usage
const pool = new WorkerPool('processor.js', 4);
// Process multiple tasks in parallel
Promise.all([
pool.runTask({ data: dataset1 }),
pool.runTask({ data: dataset2 }),
pool.runTask({ data: dataset3 }),
pool.runTask({ data: dataset4 })
]).then(results => {
console.log('All tasks complete:', results);
});
Debugging Web Workersβ
Chrome DevTools:
- Open DevTools (F12)
- Sources tab β Threads panel
- View all active workers
- Set breakpoints in worker code
- Inspect messages and data
Console logging:
// worker.js
self.onmessage = (event) => {
console.log('Worker received:', event.data);
const result = process(event.data);
console.log('Worker sending:', result);
self.postMessage(result);
};
Web Workers Best Practicesβ
1. Keep workers lightweight:
// β Don't create workers for trivial tasks
const worker = new Worker('simple-addition.js'); // Overhead too high
// β
Use for genuinely heavy tasks
const worker = new Worker('process-10mb-dataset.js');
2. Reuse workers:
// β
Create once, reuse many times
const worker = new Worker('processor.js');
button1.addEventListener('click', () => {
worker.postMessage({ task: 'task1' });
});
button2.addEventListener('click', () => {
worker.postMessage({ task: 'task2' });
});
3. Handle errors gracefully:
worker.onerror = (error) => {
console.error('Worker error:', error.message);
showUserFriendlyError();
worker.terminate();
// Optionally recreate worker
worker = new Worker('worker.js');
};
4. Clean up workers:
// Terminate workers when no longer needed
window.addEventListener('beforeunload', () => {
worker.terminate();
});
// Or after completing a one-time task
worker.onmessage = (event) => {
processResult(event.data);
worker.terminate();
};
Web Workers Checklistβ
- Identify CPU-intensive tasks blocking main thread
- Move heavy calculations to workers
- Use transferable objects for large data
- Implement error handling
- Consider worker pools for parallel tasks
- Debug with DevTools
- Terminate workers when done
- Test worker performance impact
Performance Checklistβ
A comprehensive checklist to ensure optimal web performance:
HTML & Critical Rendering Pathβ
- Keep DOM under 1,500 nodes
- Minimize DOM depth (< 32 levels)
- Use semantic HTML elements
- Inline critical CSS (above-the-fold styles)
- Defer non-critical CSS loading
- Minify and compress HTML
- Set appropriate cache headers
- Use ETags for efficient revalidation
JavaScriptβ
- Add
deferorasyncto script tags - Minify and compress JavaScript
- Implement code splitting
- Remove unused code (tree shaking)
- Use dynamic imports for heavy features
- Split vendor code into separate bundle
- Analyze bundle size regularly
- Consider modern vs. legacy bundle split
CSSβ
- Minimize render-blocking CSS
- Remove unused CSS
- Use media queries for conditional loading
- Minify and compress CSS
- Avoid
@importin CSS files - Load non-critical CSS asynchronously
Imagesβ
- Use modern formats (WebP, AVIF)
- Implement responsive images (
srcset,sizes) - Compress images (80-85% quality for JPEG)
- Set explicit width and height attributes
- Use lazy loading (
loading="lazy") - Optimize above-the-fold images
- Consider image CDN
- Optimize SVGs
Fontsβ
- Use
font-display: swaporoptional - Preload critical fonts only (1-2 max)
- Subset fonts to required characters
- Use WOFF2 format
- Consider variable fonts
- Self-host when possible
- Match fallback font metrics
Videoβ
- Use
preload="none"by default - Provide optimized poster images
- Implement lazy loading
- Use facade pattern for embeds
- Compress videos appropriately
- Provide multiple formats
- Consider adaptive streaming for long videos
Resource Loadingβ
- Use resource hints appropriately:
-
dns-prefetchfor third-party domains -
preconnectfor critical origins -
preloadfor critical resources -
prefetchfor next page resources
-
- Enable compression (Gzip/Brotli)
- Implement lazy loading
- Set up service worker caching
- Choose appropriate caching strategies
Web Workersβ
- Offload heavy computations to workers
- Use worker pools for parallel tasks
- Implement proper error handling
- Use transferable objects for large data
- Terminate workers when done
Performance Monitoringβ
- Measure Core Web Vitals:
- LCP β€ 2.5s
- INP β€ 200ms
- CLS β€ 0.1
- Monitor FCP, TTI, and TBT
- Use Lighthouse for audits
- Set performance budgets
- Track real-user metrics (RUM)
- Test on real devices and networks
Network Optimizationβ
- Use HTTP/2 or HTTP/3
- Enable connection keep-alive
- Reduce number of HTTP requests
- Implement effective caching strategy
- Use CDN for static assets
- Minimize redirect chains
Mobile Performanceβ
- Test on actual mobile devices
- Respect
Save-Dataheader - Detect connection type (
navigator.connection) - Optimize for touch interactions
- Reduce JavaScript execution time
- Consider mobile-first approach
Accessibility & SEOβ
- Ensure text remains visible during webfont load
- Provide alt text for images
- Use semantic HTML for better parsing
- Implement proper heading hierarchy
- Ensure adequate color contrast
- Make interactive elements keyboard accessible
Build & Deploymentβ
- Set up build process with minification
- Configure tree shaking
- Implement cache busting for assets
- Use service worker for offline support
- Set up continuous performance monitoring
- Document performance budget
Testingβ
- Test on 3G/4G connections
- Test on low-end devices
- Use throttling in DevTools
- Perform A/B testing for changes
- Monitor performance after deployment
Additional Resourcesβ
Toolsβ
Performance Testing:
- Lighthouse
- WebPageTest
- PageSpeed Insights
- Chrome DevTools Performance Panel
Image Optimization:
- Squoosh
- ImageOptim
- Sharp (Node.js)
Bundle Analysis:
- webpack-bundle-analyzer
- Rollup Plugin Visualizer
- source-map-explorer
Monitoring:
- Google Analytics
- Sentry Performance
- New Relic
- Datadog RUM
Further Readingβ
- web.dev Learn Performance
- MDN Web Performance
- Performance Budget Calculator
- Can I Use - Browser compatibility
Conclusionβ
Web performance optimization is an ongoing process, not a one-time task. The techniques covered in this guide provide a solid foundation for building fast, responsive websites that deliver excellent user experiences.
Key Takeaways:
- Performance is user experience - Fast sites lead to better engagement and conversions
- Measure first - Use Core Web Vitals and performance tools to identify issues
- Optimize strategically - Focus on the biggest impact items first
- Test continuously - Monitor real-user performance and iterate
- Consider all users - Test on various devices, networks, and conditions
Remember: A 1-second improvement in load time can significantly impact your site's success. Start with the basics, measure your progress, and continuously optimize.
Happy optimizing! π